using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.IO;
using System.Text.RegularExpressions;

using Servlets.Attributes;
using Servlets.Exceptions;
using Servlets.Http;
using Servlets.Api;
using Servlets.Utils;

namespace Servlets.Server
{
	/// <summary>
	/// Used to map Urls to Servlets
	/// </summary>
	class Mapping
	{
		private static string staticFilePath = Server.STATIC_ROOT;
		//private List<string> urls = new List<string>();
		private Dictionary<MappingKey, TypeMethod> map = new Dictionary<MappingKey, TypeMethod>(new MappingComparer());
		
		public Mapping() {
		}
		
		public void AddServlets(IEnumerable<Type> types) {
			foreach(Type type in types) {
				AddServlet(type);
			}
		}
		
		public void AddServlet(Type servletType) {
			lock(map) {
				if(servletType.GetCustomAttributes(typeof(ServletAttribute), true).Length < 1) {
					throw new ClassNotServletException();
				}
				
				var methods = servletType.GetMethods();
				
				foreach(var method in methods) {
					AddToMap<HeadMethodAttribute>	(servletType, method, HttpMethod.Head);
					AddToMap<GetMethodAttribute>	(servletType, method, HttpMethod.Get);
					AddToMap<PostMethodAttribute>	(servletType, method, HttpMethod.Post);
					AddToMap<PutMethodAttribute>	(servletType, method, HttpMethod.Put);
					AddToMap<DeleteMethodAttribute>	(servletType, method, HttpMethod.Delete);
					AddToMap<TraceMethodAttribute>	(servletType, method, HttpMethod.Trace);
					AddToMap<OptionsMethodAttribute>(servletType, method, HttpMethod.Options);
					AddToMap<ConnectMethodAttribute>(servletType, method, HttpMethod.Connect);
					AddToMap<PathMethodAttribute>	(servletType, method, HttpMethod.Path);
				}
			}
		}
		
		private void AddToMap<T>(Type servletType, MethodInfo method, HttpMethod httpmethod)
			where T : ServletMethodAttribute
		{
			var attributes = method.GetCustomAttributes(typeof(T), true);
			foreach(var attribute in attributes) {
				string url = (attribute as T).UrlMapping;
				Log.WriteLine("[INFO] Mapping method '{0}' of type '{1}' to '{2}'", method.Name, httpmethod, url);
				
				MappingKey key = new MappingKey(url, httpmethod);
				TypeMethod val = new TypeMethod(servletType, method);
				
				if(map.ContainsKey(key)) {
					throw new DuplicateUrlMappingException(url, httpmethod);
				}
				
				map[key] = val;
				//urls.Add(url);
			}
		}
		
		public ServletInvoker GetServletByUrl(string givenUrl, HttpMethod method) {
			lock(map) {
				MappingKey key = FindUrlMatch(givenUrl, method);
				
				if(key == null) {
					//No servlet mapped. Check if there's a static file we can use
					return GetStaticFileServlet(givenUrl);
				}
				
				if(key.Method != method) {
					throw new MethodNotAllowedException(method, givenUrl);
				}
				TypeMethod typeMethod = map[key];
				
				Type servletType = typeMethod.Type;
				MethodInfo methodInfo = typeMethod.Method;
				Object servlet = Activator.CreateInstance(servletType);
				string[] parameters = key.GetParameters(givenUrl);
				
				ServletInvoker invoker = new ServletInvoker(servlet, methodInfo, parameters);
				return invoker;
			}
		}
		
		private ServletInvoker GetStaticFileServlet(string givenUrl) {
			lock(map) {
				string path = Mapping.staticFilePath + givenUrl;
				if(File.Exists(path)) {
					object servlet = new StaticFileServlet();
					MethodInfo method = StaticFileServlet.StaticFileGetMethod();
					ServletInvoker invoker = new ServletInvoker(servlet, method);
					return invoker;
				}
				throw new NotFoundException(givenUrl);
			}
		}
		
		public void PrintAllMapped() {
			Log.WriteLine("** All Mapped");
			Log.WriteLine("*** Keys");
			foreach(var key in map.Keys) {
				MappingKey newKey = new MappingKey(key.Url, key.Method);
				
				Log.Write(newKey + ": ");
				Log.WriteLine(map[key]);
			}
		}
		
		/// <summary>
		/// Finds the *best* mapped url including wildcards for the given url.
		/// Returns null if there is no matching URL
		/// If there is not a url/method pair that matches, but yet there is a Url that matches, that pair will be returned.
		/// </summary>
		/// <param name="givenUrl">
		/// A <see cref="System.String"/>. The url from the http request.
		/// </param>
		/// <returns>
		/// A <see cref="System.String"/>. Returns a url that was mapped to a Servlet
		/// </returns>
		private MappingKey FindUrlMatch(string givenUrl, HttpMethod method) {
			MappingKey temp = null;
			MappingKey regexMatch = null;
			foreach(MappingKey key in map.Keys) {
				if(key.UrlRegex.IsMatch(givenUrl)) {
					if(key.Method == method) {
						if(key.Url == givenUrl) {
							return key;
						} else {
							regexMatch = key; //lets just make sure there's not a better match
						}
					} else if (temp == null) {
						//this is ugly. But I needed a quick way to distinguish between a 404 and Method Not Permitted.
						temp = key;
					}
				}
			}
			if(regexMatch == null) {
				return temp;
			} else {
				return regexMatch;
			}
		}
	}
	
	class TypeMethod {
		public Type Type {get;private set;}
		public MethodInfo Method {get;private set;}
		
		public TypeMethod(Type type, MethodInfo method) {
			if(type == null) {
				throw new InvalidParameterException("type");
			}
			if(method == null) {
				throw new InvalidParameterException("method");
			}
			
			this.Type = type;
			this.Method = method;
		}
		
		public override bool Equals(object obj)
		{
			TypeMethod key = obj as TypeMethod;
			if(key == null) return false;
			if(key.Method != this.Method) return false;
			if(key.Type != this.Type) return false;
			return true;
		}
		
		public override int GetHashCode()
		{
			return Type.GetHashCode();
		}
		
		public static bool operator==(TypeMethod key1, TypeMethod key2) {
			return key1.Equals(key2);
		}
		
		public static bool operator!=(TypeMethod key1, TypeMethod key2) {
			return !(key1 == key2);
		}
		
		public override string ToString()
		{
			return String.Format("type:[{0} method:{1}]", Type.Name, Method.Name);
		}
	}
	
	class MappingKey {
		public string Url {get; private set;}
		public Regex UrlRegex {get; private set;}
		public HttpMethod Method {get; private set;}
		
		
		public string[] GetParameters(string givenUrl) {
			return GetParametersFromUrl(givenUrl).ToArray();
		}
		
		private IEnumerable<string> GetParametersFromUrl(string url) {
			Match match = UrlRegex.Match(url);
			
			bool first = true;
			foreach(Group group in match.Groups) {
				if(first) {
					first = false;
					continue;
				}
				string value = group.Value;
				yield return value;
			}
		}
		
		public MappingKey(string url, HttpMethod method) {
			if(url == null) {
				throw new InvalidParameterException("url");
			}
			
			this.Url = url;
			String tempUrl = url.RegexReplace(@"\$([^/]+)\$", @"([^/]+)");
			this.UrlRegex = new Regex("^"+tempUrl+"$");
			this.Method = method;
		}
				
		public override bool Equals(object obj)
		{
			MappingKey key = obj as MappingKey;
			if(key == null) return false;
			if(key.Method != this.Method) return false;
			if(key.Url != this.Url) return false;
			return true;
		}
		
		public override int GetHashCode()
		{
			return Url.GetHashCode() ^ Method.GetHashCode();
		}
				
		public override string ToString()
		{
			return String.Format("[url:{0} httpmethod:{1}]", Url, Method);
		}
	}
	
	class MappingComparer : IEqualityComparer<MappingKey> {
		public bool Equals(MappingKey x, MappingKey y)
		{
			if(x == null && y == null)
				return true;
			if(x == null)
				return false;
			if(y == null)
				return false;
			return x.Equals(y);
		}
		
		public int GetHashCode(MappingKey obj)
		{
			return obj.GetHashCode();
		}
	}
}